/*
Copyright 2011-2012 Anthony Zhang <azhang9@gmail.com>

this file is part of ProgressPlatformer. Source code is available at <https://github.com/Uberi/ProgressPlatformer>.

ProgressPlatformer is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.

this program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU Affero General Public License for more details.

You should have received a copy of the GNU Affero General Public License
along with this program.  if not, see <http://www.gnu.org/licenses/>.
*/

; 2025/08/31 xcatp 语法升为 v2
#Include g:\AHK\git-ahk-lib\Extend.ahk


class NotePlayer {

  __New() {
    this.Device := MIDIOutputDevice()
    this.Device.SetVolume(100)

    this.pCallback := CallbackCreate((*) => this.SequenceCallback(), 'F')
    this.Offset := 1, this.Timeline := [], this.Playing := false, this.Repeat := false
  }

  Instrument(sound) {
    offset := Round(this.Offset)
    if !this.Timeline.Has(offset)
      ExtendArrSize(this.Timeline, offset), this.Timeline[offset] := []
    this.Timeline[offset].Push({ Type: 'Instrument', Sound: sound })
    return this
  }

  Delay(len) {
    if (this.Offset + len) < 0
      throw Error('Invalid offset: ' . (this.Offset + len))
    this.Offset += len
    return this
  }

  Note(index, length, downVelocity := 60, upVelocity := 60) {
    offset := Round(this.Offset)
    if !this.Timeline.Has(offset)
      ExtendArrSize(this.Timeline, offset), this.Timeline[offset] := []
    this.Timeline[offset].Push({ Type: 'NoteOn', Index: index, Velocity: downVelocity })

    endOffset := Round(this.Offset + length)
    if !this.Timeline.Has(endOffset)
      ExtendArrSize(this.Timeline, endOffset), this.Timeline[endOffset] := []
    this.Timeline[endOffset].Push({ Type: 'NoteOff', Index: index, Velocity: upVelocity })
    return this
  }


  Play(index, length, sound, downVelocity := 60, upVelocity := 60) {
    ; play the note with the given sound
    previousSound := this.Device.Sound
    this.Device.Sound := sound
    this.Device.NoteOn(index, downVelocity)
    this.Device.Sound := previousSound

    ; set up the data the timer will need to do its task
    timerData := Object(), ObjAddRef(&timerData)
    timerData.Index := index, timerData.Velocity := upVelocity, timerData.Device := this.Device

    ; set up a timer to turn off the note
    pCallback := CallbackCreate((*) => this.PlayCallback(timerData), 'F')
    hTimer := DllCall('SetTimer', 'UPtr', 0, 'UPtr', 0, 'UInt', length, 'UPtr', pCallback, 'UPtr')
    if !hTimer
      throw Error('Could not create update timer.')

    timerData.pCallback := pCallback
    timerData.hTimer := hTimer

    return this
  }

  Start() {
    if this.Playing ; note player is already playing
      return this

    ; insert the ending delay if necessary
    if !this.Timeline.Has(this.Offset)
      ExtendArrSize(this.Timeline, this.offset), this.Timeline[this.Offset] := []

    ; convert the timeline into a set of actions
    this.Sequence := [], PreviousOffset := 0
    for offset, actions In this.Timeline {
      if !IsSet(actions)
        continue
      CurrentActions := actions.Clone()
      CurrentActions.InsertAt(1, offset - PreviousOffset)
      PreviousOffset := offset
      this.Sequence.Push(CurrentActions)
    }

    ; activate the first set of actions if it is present
    if this.Sequence.Length {
      this.ActiveNotes := []
      this.Playing := True
      this.Index := 1

      ; set up a timer to execute the action set
      this.hTimer := DllCall('SetTimer', 'UPtr', 0, 'UPtr', 0, 'UInt', this.Sequence[1][1], 'UPtr', this.pCallback,)
      if !this.hTimer
        throw Error('Could not create update timer.')
    }
    return this
  }

  Stop() {
    if !this.Playing
      return this

    ; clean up timers
    if !DllCall('KillTimer', 'UPtr', 0, 'UPtr', this.hTimer)
      throw Error('Could not destroy update timer.')

    ; turn off any active notes
    for Index In this.ActiveNotes
      this.Device.NoteOff(Index, 100)
    this.Playing := false
    return this
  }

  Reset() {
    this.Stop()
    this.Offset := 0
    this.Timeline := []
    this.Sequence := []
    return this
  }

  SequenceCallback() {
    ; remove the currently active timer
    if !DllCall('KillTimer', 'UPtr', 0, 'UPtr', this.hTimer)
      throw Error('Could not destroy update timer.')

    ; execute the set of actions
    for idx, action In this.Sequence[this.Index] { ; perform the current action set

      if idx != 1 { ; skip over the first field, which is the time offset

        if action.Type = 'Instrument' ; instrument change
          this.Device.Sound := action.Sound
        else if action.Type = 'NoteOn' { ; note on action
          this.Device.NoteOn(action.Index, action.Velocity)
          if !this.ActiveNotes.Length < action.Index
            this.ActiveNotes.Length := action.index, this.ActiveNotes[action.Index] := 1
        } else if action.Type = 'NoteOff' { ; note off action
          this.Device.NoteOff(action.Index, action.Velocity)
          if this.ActiveNotes.Has(action.Index)
            this.ActiveNotes.RemoveAt(action.Index)
        }
      }
    }

    if (this.Index < this.Sequence.Length) { ; set the next timer if available
      this.Index ++
      this.hTimer := DllCall('SetTimer', 'UPtr', 0, 'UPtr', 0, 'UInt', this.Sequence[this.Index][1], 'UPtr', this.pCallback, 'UPtr')
    } else if this.Repeat { ; repeat note sequence
      this.Index := 1
      this.hTimer := DllCall('SetTimer', 'UPtr', 0, 'UPtr', 0, 'UInt', this.Sequence[1][1], 'UPtr', this.pCallback, 'UPtr')
    } else { ; stop playing

      ; turn off any active notes
      for idx In this.ActiveNotes {
        if IsSet(idx)
          this.Device.NoteOff(idx, 100)
      }
      this.Playing := false
    }
  }

  PlayCallback(TimerData) {
    if !DllCall('KillTimer', 'UPtr', 0, 'UPtr', TimerData.hTimer)
      throw Error('Could not destroy update timer.')
    DllCall('GlobalFree', 'UPtr', TimerData.pCallback) ; free callback

    TimerData.Device.NoteOff(TimerData.Index, TimerData.Velocity)
  }

  __Delete() {
    this.Stop()
    DllCall('GlobalFree', 'UPtr', this.pCallback) ; free callback
  }
}


class MIDIOutputDevice {

  static DeviceCount := 0

  __New(DeviceID := 0) {
    if MIDIOutputDevice.DeviceCount = 0 {
      this.hModule := DllCall('LoadLibrary', 'Str', 'winmm')
      if !this.hModule
        throw Error('Could not load WinMM library.')
    }
    MIDIOutputDevice.DeviceCount++

    ; open the MIDI output device
    hMIDIOutputDevice := 0
    Status := DllCall('winmm\midiOutOpen', 'UInt*', &hMIDIOutputDevice, 'UInt', DeviceID, 'UPtr', 0, 'UPtr', 0, 'UInt', 0) ; CALLBACK_NULL
    if Status != 0 ; MMSYSERR_NOERROR
      throw Error('Could not open MIDI output device: ' . DeviceID . '.')
    this.hMIDIOutputDevice := hMIDIOutputDevice

    this.SetChannel(0)
    this.SetSound(0)
    this.SetPitch(0)
  }

  SetChannel(v) {
    if !v.Between(0, 15)
      throw Error('Invalid channel: ' . v . '.', -1)
    this.channel := v
  }

  SetSound(v) {
    if !v.Between(0, 127)
      throw Error('Invalid sound: ' . v . '.', -1)
    if DllCall('winmm\midiOutShortMsg', 'UInt', this.hMIDIOutputDevice, 'UInt', 0xC0 | this.channel | (v << 8)) ; 'Program Change' event
      throw Error('Could not send Program Change message.')
    this.sound := v
  }

  SetPitch(v) {
    v := Clamp(v, -100, 100)
    TempValue := Round(((v + 100) / 200) * 0x4000)
    if DllCall('winmm\midiOutShortMsg', 'UInt', this.hMIDIOutputDevice, 'UInt', 0xE0 | this.channel | ((TempValue & 0x7F) << 8) | (TempValue << 9)) ; 'Pitch Bend' event
      throw Error('Could not send Pitch Bend message.')
    this.pitch := v
  }

  __Delete() {
    this.Reset()
    if DllCall('winmm\midiOutClose', 'UInt', this.hMIDIOutputDevice)
      throw Error('Could not close MIDI output device.')

    MIDIOutputDevice.DeviceCount--
    if MIDIOutputDevice.DeviceCount = 0
      DllCall('FreeLibrary', 'UPtr', this.hModule)
  }

  GetVolume(channel := '') {
    Volume := 0
    if DllCall('winmm\midiOutGetVolume', 'UInt', this.hMIDIOutputDevice, 'UInt*', Volume) ; retrieve the device volume
      throw Error('Could not retrieve device volume.')
    if (channel = '' || channel = 'Left')
      return ((Volume & 0xFFFF) / 0xFFFF) * 100
    else if (channel = 'Right')
      return ((Volume >> 16) / 0xFFFF) * 100
    else
      throw Error('Invalid channel:' . channel . '.', -1)
  }

  SetVolume(volume, channel := '') {
    if !volume.Between(0, 100)
      throw Error('Invalid volume: ' . volume . '.', -1)
    if (channel = '')
      volume := Round((volume / 100) * 0xFFFF), volume |= volume << 16
    else if (channel = 'Left')
      volume := Round((volume / 100) * 0xFFFF)
    else if (channel = 'Right')
      volume := Round((volume / 100) * 0xFFFF) << 16
    else
      throw Error('Invalid channel: ' . channel . '.', -1)
    DllCall('winmm\midiOutSetVolume', 'UInt', this.hMIDIOutputDevice, 'UInt', volume) ; set the device volume
  }

  NoteOn(Note, Velocity) {
    if !IsInteger(Note)
      throw Error('Invalid note: ' . Note . '.', -1)
    if !Velocity.Between(0, 100)
      throw Error('Invalid velocity: ' . Velocity . '.', -1)
    Velocity := Round((Velocity / 100) * 127)
    if DllCall('winmm\midiOutShortMsg', 'UInt', this.hMIDIOutputDevice, 'UInt', 0x90 | this.Channel | (Note << 8) | (Velocity << 16)) ; 'Note On' event
      throw Error('Could not send Note On message.')
  }

  NoteOff(Note, Velocity) {
    if !IsInteger(Note)
      throw Error('Invalid note: ' . Note . '.', -1)
    if !Velocity.Between(0, 100)
      throw Error('Invalid velocity: ' . Velocity . '.', -1)
    Velocity := Round((Velocity / 100) * 127)
    if DllCall('winmm\midiOutShortMsg', 'UInt', this.hMIDIOutputDevice, 'UInt', 0x80 | this.Channel | (Note << 8) | (Velocity << 16)) ; 'Note Off' event
      throw Error('Could not send Note Off message.')
  }

  UpdateNotePressure(Note, Pressure) {
    if !IsInteger(Note)
      throw Error('Invalid note: ' . Note . '.', -1)
    if !Pressure.Between(0, 100)
      throw Error('Invalid pressure: ' . Pressure . '.', -1)
    Pressure := Round((Pressure / 100) * 127)
    if DllCall('winmm\midiOutShortMsg', 'UInt', this.hMIDIOutputDevice, 'UInt', 0xA0 | this.Channel | (Note << 8) | (Pressure << 16)) ; 'Polyphonic Aftertouch' event
      throw Error('Could not send Polyphonic Aftertouch message.')
  }

  Reset() {
    if DllCall('winmm\midiOutReset', 'UInt', this.hMIDIOutputDevice)
      throw Error('Could not reset MIDI output device.')
  }
}